﻿using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.IO;
using Argos.Design;
using System.Drawing.Design;
using System.ComponentModel.Design;
using System.Runtime.Serialization;
using System.Workflow.Activities;
using Argos.Workflows;
using Argos.Windows.Forms;
using System.Xml.Serialization;
using System.Configuration;
using System.Xml.Schema;
using System.Xml;

namespace Argos.Data.Model
{
	/// <summary>
	/// Represents a target application or system that is being monitored by Argos.
	/// </summary>
	[Serializable]
	[DefaultProperty("Name")]
	[XmlRoot("target", Namespace = ItemBase.XmlNamespace)]
	public sealed class Target : ItemBase , IXmlSerializable , IEquatable<Target>
	{

		#region #ctor: public Target()

		/// <summary>
		/// Creates a new instance of the class.
		/// </summary>
		public Target()
		{
			this.Enabled = true;
			this.TestExecutionInterval = TimeSpan.FromSeconds(30);
		}

		#endregion



		#region public string Description { get; set; }

		private static string DescriptionPropertyName = "Description";

		/// <summary>
		/// Sets or returns the description for the target.
		/// </summary>
		[Category("Identification")]
		[DisplayName("Description")]
		[Description("Specifies the description for the target. The description is included in alerts sent from the target.")]
		[DefaultValue((string)null)]
		[Editor(typeof(MultilineStringEditor), typeof(UITypeEditor))]
		public string Description
		{
			get { return this.GetProperty<string>(DescriptionPropertyName); }
			set { this.SetProperty(DescriptionPropertyName, value); }
		}

		#endregion

		#region public bool Enabled { get; set; }

		private static string EnabledPropertyName = "Enabled";

		/// <summary>
		/// Sets or returns whether monitoring is enabled on the target.
		/// </summary>
		[Category("Test")]
		[Description("Specifies whether the target is enabled. If a target is not enabled, the test specified for the target will not be executed.")]
		[DefaultValue(true)]
		public bool Enabled
		{
			get { return this.GetProperty<bool>(EnabledPropertyName); }
			set { this.SetProperty(EnabledPropertyName, value); }
		}

		#endregion

		#region public Dictionary<string, object> MonitoringWorkflowProperties { get; set; }

		private static string MonitoringWorkflowPropertiesPropertyName = "MonitoringWorkflowProperties";

		/// <summary>
		/// Returns a dictionary containing the properties that are set
		/// on any new instance that is created out of the type specified
		/// in <see cref="MonitoringWorkflowType"/>.
		/// </summary>
		[Category("Monitoring")]
		[DisplayName("Monitor Properties")]
		[Description("Specifies the properties for the monitor specified in 'Monitor'.")]
		[Editor(typeof(TargetMonitorPropertiesTypeEditor), typeof(UITypeEditor))]
		public Dictionary<string, object> MonitoringWorkflowProperties
		{
			get
			{
				Dictionary<string, object> props = this.GetProperty<Dictionary<string, object>>(MonitoringWorkflowPropertiesPropertyName);
				if (null == props)
				{
					props = new Dictionary<string, object>();
					this.SetProperty(MonitoringWorkflowPropertiesPropertyName, props);
				}
				return props;
			}
			set { this.SetProperty(MonitoringWorkflowPropertiesPropertyName, value); }
		}

		#endregion
		
		#region public Type MonitoringWorkflowType { get; set; }

		private static string MonitoringWorkflowTypePropertyName = "MonitoringWorkflowType";

		/// <summary>
		/// Sets or returns the type of the monitoring workflow that is used to monitor
		/// the target.
		/// </summary>
		[Category("Monitoring")]
		[DisplayName("Monitor")]
		[Description("Specifies the type of monitoring to use when monitoring the target.")]
		[Editor(typeof(MonitoringWorkflowTypeEditor), typeof(UITypeEditor))]
		[RefreshProperties(RefreshProperties.All)]
		public Type MonitoringWorkflowType
		{
			get { return this.GetProperty<Type>(MonitoringWorkflowTypePropertyName); }
			set
			{
				if(null == value)
				{
					throw new ArgumentNullException("MonitoringWorkflowType");
				}
				if (!value.IsTargetMonitoringWorkflow())
				{
					throw new ArgumentException("The given type does not meet the criteria for target monitoring workflows.", "MonitoringWorkflowType");
				}

				if (!object.Equals(this.MonitoringWorkflowType, value))
				{
					this.MonitoringWorkflowProperties.Clear();
					this.SetProperty(MonitoringWorkflowTypePropertyName, value);
				}
			}
		}

		#endregion

		#region public string Name { get; set; }

		private static string NamePropertyName = "Name";

		/// <summary>
		/// Sets or returns the name of the target.
		/// </summary>
		[Category("Identification")]
		[DisplayName("Name")]
		[Description("Specifies the name of the target.")]
		[DefaultValue((string)null)]
		public string Name
		{
			get { return this.GetProperty<string>(NamePropertyName); }
			set { this.SetProperty(NamePropertyName, value); }
		}

		#endregion

		#region public Test Test { get; }

		private static string TestPropertyName = "Test";

		/// <summary>
		/// Returns the test for the target.
		/// </summary>
		[Category("Test")]
		[Description("Defines the test that is periodically executed to test the target.")]
		[DefaultValue((Test)null)]
		[TypeConverter(typeof(ExpandableObjectConverter))]
		public Test Test
		{
			get
			{
				Test t = this.GetProperty<Test>(TestPropertyName);
				if (null == t)
				{
					t = new Test();
					this.SetProperty(TestPropertyName, t);
				}
				return t;
			}
		}

		#endregion

		#region public TimeSpan TestExecutionInterval { get; set; }

		private static string TestExecutionIntervalPropertyName = "TestExecutionInterval";

		/// <summary>
		/// Returns the interval between test runs in for the target.
		/// </summary>
		[Category("Test")]
		[DisplayName("Test Interval")]
		[Description("Specifies how often the test for the target should be executed.")]
		public TimeSpan TestExecutionInterval
		{
			get { return this.GetProperty<TimeSpan>(TestExecutionIntervalPropertyName); }
			set { this.SetProperty(TestExecutionIntervalPropertyName, value); }
		}

		#endregion



		#region public static Target Read(Stream stream)

		/// <summary>
		/// Reads the target stored in the given stream and returns it.
		/// </summary>
		/// <param name="stream">The stream to read the target from.</param>
		public static Target Read(Stream stream)
		{
			return ItemBase.Read<Target>(stream);
		}

		#endregion

		#region public static Target Read(FileInfo sourceFile)

		/// <summary>
		/// Reads the target stored in the given file and returns it.
		/// </summary>
		/// <param name="sourceFile">The file to read the target from.</param>
		public static Target Read(FileInfo sourceFile)
		{
			return ItemBase.Read<Target>(sourceFile);
		}

		#endregion

		#region public static Target Read(byte[] buffer)

		/// <summary>
		/// Reads the target stored in the given buffer and returns it.
		/// </summary>
		/// <param name="buffer">The byte array to read the target from.</param>
		public static Target Read(byte[] buffer)
		{
			return ItemBase.Read<Target>(buffer);
		}

		#endregion

		#region public static Target ReadXml(Stream stream)

		/// <summary>
		/// Reads the XML stored in the given stream and deserializes it
		/// to a new <see cref="Target"/> instance.
		/// </summary>
		/// <param name="stream">The stream to read from.</param>
		public static Target ReadXml(Stream stream)
		{
			return ItemBase.ReadXml<Target>(stream);
		}

		#endregion

		#region public static Target ReadXml(FileInfo sourceFile)

		/// <summary>
		/// Reads the XML stored in the given file and deserializes it
		/// to a new <see cref="Target"/> instance.
		/// </summary>
		/// <param name="sourceFile">The file to read the XML from.</param>
		public static Target ReadXml(FileInfo sourceFile)
		{
			return ItemBase.ReadXml<Target>(sourceFile);
		}

		#endregion



		#region public override void InsertOnSubmit(Argos.Data.SqlServer.ArgosDatabase context)

		/// <summary>
		/// Inserts the current target to the given context when the changes made to that
		/// context are submitted.
		/// </summary>
		/// <param name="context">The data context to insert the current target to.</param>
		public override void InsertOnSubmit(Argos.Data.SqlServer.ArgosDatabase context)
		{
			Argos.Data.SqlServer.Target target = new Argos.Data.SqlServer.Target();

			#region Target

			target.TargetId = this.Id;
			target.Description = this.Description;
			target.Enabled = this.Enabled;
			target.MonitoringWorkflowType = this.MonitoringWorkflowType.FullName;
			foreach (var item in this.MonitoringWorkflowProperties)
			{
				if (null != item.Value)
				{
					target.MonitoringWorkflowProperties.Add(
						new Argos.Data.SqlServer.MonitoringWorkflowProperty()
						{
							Property = new Argos.Data.SqlServer.Property()
							{
								PropertyId = Guid.NewGuid(),
								Name = item.Key,
								Value = new System.Data.Linq.Binary(item.Value.SerializeToBinary()),
								ValueType = item.Value.GetType().FullName
							}
						}
					);
				}
			}

			target.Name = this.Name;
			target.TestExecutionInterval = (int)this.TestExecutionInterval.TotalSeconds;

			#endregion


			Argos.Data.SqlServer.Test test = new Argos.Data.SqlServer.Test();

			#region Test

			test.TestId = this.Test.Id;
			test.TestActivityType = this.Test.ActivityType.FullName;
			foreach (var item in this.Test.ActivityProperties)
			{
				if (null != item.Value)
				{
					test.TestActivityProperties.Add(
						new Argos.Data.SqlServer.TestActivityProperty()
						{
							Property = new Argos.Data.SqlServer.Property()
							{
								PropertyId = Guid.NewGuid(),
								Name = item.Key,
								Value = item.Value.SerializeToBinary(),
								ValueType = item.Value.GetType().FullName
							}
						}
					);
				}
			}

			test.TestExecutionTimeout = (int)this.Test.ExecutionTimeout.TotalMilliseconds;

			#endregion

			target.Test = test;

			context.Targets.InsertOnSubmit(target);
		}

		#endregion

		#region public override bool Equals(object obj)

		/// <summary>
		/// Compares the current target to the given object.
		/// </summary>
		/// <param name="obj">The object to compare to the current target.</param>
		/// <returns>Returns <c>true</c> if the given object equals the current target.</returns>
		public override bool Equals(object obj)
		{
			if (obj is Target)
			{
				return this.Equals((Target)obj);
			}
			return false;
		}

		#endregion
		
		#region public bool Equals(Target other)

		/// <summary>
		/// Compares the given target to the current target.
		/// </summary>
		/// <param name="other">The target to compare to the current target.</param>
		/// <returns>Returns <c>true</c> if the given target equals the current target.</returns>
		public bool Equals(Target other)
		{
			if (null == other)
				return false;

			if (!Guid.Equals(this.Id, other.Id))
				return false;

			if (!string.Equals(this.Description, other.Description))
				return false;

			if (!bool.Equals(this.Enabled, other.Enabled))
				return false;

			List<string> thisKeyList = new List<string>(this.MonitoringWorkflowProperties.Keys);
			List<string> thatKeyList = new List<string>(other.MonitoringWorkflowProperties.Keys);

			if (thisKeyList.Count != thatKeyList.Count)
				return false;

			for (int i = 0; i < thisKeyList.Count; i++)
			{
				if (thisKeyList[i] != thatKeyList[i])
					return false;
			}

			List<object> thisValueList = new List<object>(this.MonitoringWorkflowProperties.Values);
			List<object> thatValueList = new List<object>(other.MonitoringWorkflowProperties.Values);

			if (thisValueList.Count != thatValueList.Count)
				return false;

			for (int i = 0; i < thisValueList.Count; i++)
			{
				if (!object.Equals(thisValueList[i], thatValueList[i]))
					return false;
			}

			if (!Type.Equals(this.MonitoringWorkflowType, other.MonitoringWorkflowType))
				return false;

			if (!string.Equals(this.Name, other.Name))
				return false;

			if (!this.Test.Equals(other.Test))
				return false;

			if (!TimeSpan.Equals(this.TestExecutionInterval, other.TestExecutionInterval))
				return false;

			return true;
		}

		#endregion

		#region public override int GetHashCode()

		/// <summary>
		/// Returns the hash code for the current target.
		/// </summary>
		public override int GetHashCode()
		{
			return base.GetHashCode();
		}

		#endregion

		#region public void ReadXml(XmlReader reader)

		/// <summary>
		/// Reads the XML stored in the given XML reader and populates the members
		/// of the current instance with that data.
		/// </summary>
		/// <param name="reader">The reader to read from.</param>
		public void ReadXml(XmlReader reader)
		{
			if (ItemBase.XmlNamespace != reader.NamespaceURI)
			{
				throw new InvalidOperationException(string.Format("Unrecognized namespace: '{0}'.", reader.NamespaceURI));
			}

			if (reader.LocalName != "target")
			{
				throw new InvalidOperationException(string.Format("Unrecognized element name: '{0}'.", reader.LocalName));
			}

			Dictionary<string, object> properties = new Dictionary<string, object>();

			reader.ReadStartElement();

			while (reader.NodeType == XmlNodeType.Element)
			{
				switch (reader.LocalName)
				{
					case "id":
						this.Id = new Guid(reader.ReadElementContentAsString());
						break;

					case "description":
						this.Description = reader.ReadElementContentAsString();
						break;

					case "enabled":
						this.Enabled = reader.ReadElementContentAsBoolean();
						break;

					case "monitoringWorkflowProperties":
						reader.ReadElementContent(properties, "property", "name", "value");
						break;

					case "monitoringWorkflowType":
						Type t = reader.ReadElementContentAsType();
						if (null != t)
							this.MonitoringWorkflowType = t;
						break;

					case "name":
						this.Name = reader.ReadElementContentAsString();
						break;

					case "test":
						this.Test.ReadXml(reader);
						break;

					case "testExecutionInterval":
						this.TestExecutionInterval = TimeSpan.Parse(reader.ReadElementContentAsString());
						break;

					default:
						throw new InvalidOperationException(string.Format("Unrecognized element: '{0}'.", reader.LocalName));
				}
			}

			reader.ReadEndElement();

			this.MonitoringWorkflowProperties = properties;
		}

		#endregion

		#region public void WriteXml(XmlWriter writer)

		/// <summary>
		/// Writes the current target instance as XML to the given writer.
		/// </summary>
		/// <param name="writer">The writer to write to.</param>
		public void WriteXml(XmlWriter writer)
		{
			writer.WriteElementString("id", this.Id.ToString());

			if (!string.IsNullOrEmpty(this.Description))
				writer.WriteElementString("description", this.Description);

			writer.WriteElementString("enabled", this.Enabled.ToString().ToLower());

			writer.WriteDictionaryElement(this.MonitoringWorkflowProperties, "monitoringWorkflowProperties", "property", "name", "value");

			if (null != this.MonitoringWorkflowType)
				writer.WriteTypeElement(this.MonitoringWorkflowType, "monitoringWorkflowType");

			if (null != this.Name)
				writer.WriteElementString("name", this.Name);

			writer.WriteStartElement("test");
			this.Test.WriteXml(writer);
			writer.WriteEndElement();

			writer.WriteElementString("testExecutionInterval", this.TestExecutionInterval.ToString());
		}

		#endregion



		#region XmlSchema IXmlSerializable.GetSchema()

		XmlSchema IXmlSerializable.GetSchema()
		{
			throw new NotImplementedException();
		}

		#endregion

	}

}
